package org.barad.architecture.analyzer.archpad;

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileFilter;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.util.HashMap;

import javax.swing.DefaultListModel;
import javax.swing.JFileChooser;
import javax.swing.JList;
import javax.swing.JOptionPane;

import org.apache.log4j.Logger;
import org.barad.architecture.analyzer.archpad.component.ArchPadComponent;
import org.barad.architecture.analyzer.archpad.persistence.PersistenceManager;
import org.jgraph.JGraph;
import org.jgraph.graph.DefaultGraphCell;
import org.jgraph.graph.GraphLayoutCache;

/**
 * This class contains actions that are performed from the GUI.
 * The event listeners registered for events in the GUI delegate
 * to methods of this class.
 * 
 * @author svetoslavganov@gmail.com (Svetoslav R. Ganov)
 *
 */
public class Actions {
	private Logger log;
	private File modelFile;
	private boolean modelModified;
	private File componentLibraryPath;
	private ArchPad archPad;
	private HashMap<String, Object[]> componentLibraryCache;
	private PersistenceManager persistenceManager;

	/**
	 * Constructs an {@link Actions} instance.
	 * 
	 * @param archPad Reference to the corresponding {@link ArchPad}
	 *        instance.
	 */
	public Actions(ArchPad archPad) {
		this.archPad = archPad;
		this.log = Logger.getLogger(getClass());
		this.componentLibraryCache = new HashMap<String, Object[]>();
		this.persistenceManager = new PersistenceManager();
	}

	/**
	 * Gets the component library path.
	 * 
	 * @return The component library path.
	 */
	public File getComponentLibraryPath() {
		return componentLibraryPath;
	}
	
	/**
	 * Sets the component library path by providing a GUI to
	 * the user from which the user can select the library folder. 
	 */
	public void setComponentLibraryPath() {
		componentLibraryPath = chooseFile(EntityType.COMPONENT_LIBRARY_PATH, 
				OperationType.SET_COMPONENT_LIBRARY_PATH, FileType.FOLDER);
	}

	/**
	 * Sets the component library path.
	 * 
	 * @param componentLibraryPath The component library path.
	 */
	public void setComponentLibraryPath(File componentLibraryPath) {
		this.componentLibraryPath = componentLibraryPath;
	}
	
	/**
	 * Saves a component.
	 * 
	 * TODO(Svetoslav R. Ganov): Implement
	 */
	public void saveComponent() {
		
	}

	/**
	 * Creates a new ArchPad model.
	 */
	public void newModel() {
		JGraph jGraph = archPad.getCustomizedJGraph();
		if (jGraph.getRoots().length > 0) {
			if (modelFile == null) {
				int option = JOptionPane.showConfirmDialog(null, 
						"Do you want to save the changes ??");
				if (option == 0) {
					saveModel();
				}
				Object[] roots = jGraph.getDescendants(jGraph.getRoots());
				jGraph.getModel().remove(roots);
			}
		}
		archPad.setTitle("Untitled - ArchPad");
		archPad.getRegionManager().clearRegions();
	}
	
	/**
	 * Shows a GUI to the user from which the user selects destination
	 * to which to save a file. The GUI is checking if the file already
	 * exists and in such a case interacts with the user appropriately.
	 * 
	 * @param file The file to be saved.
	 * @return True if the file was saved, false otherwise.
	 */
	public boolean saveToSelectedFile(File file) {
		if (file.exists() && file.isFile()) {
			String message = "The selected file exists! Do you want to overwrite it?";
			int result = JOptionPane.showConfirmDialog(null, message, 
					"Selected file exists", JOptionPane.OK_CANCEL_OPTION);
			if (result == JOptionPane.CANCEL_OPTION) {
				 return false;
			}
		}
		return true;
	}

	/**
	 * Saves an architectural component as a new component, 
	 * i.e. in a new file.
	 */
	public void saveComponentAs() {
		JGraph jGraph = archPad.getCustomizedJGraph();
		Object[] selectedCells = jGraph.getSelectionCells();
		// we save as components only selected subgraphs
		if (selectedCells != null) {
			File file = chooseFile(EntityType.COMPONENT, 
					OperationType.SAVE_AS, FileType.COMPONENT);
			if (file != null && saveToSelectedFile(file)) {
				DefaultGraphCell parent = null;
				if (selectedCells.length == 1) {
					parent = (DefaultGraphCell) selectedCells[0];
				} else {
					// group the selection as a component if more than one vertex is selected
					parent = archPad.getComponentManager().groupAsComponent(selectedCells);
				}
				// save the component
				saveCells(file, new Object[] {parent}, EntityType.COMPONENT, 
						FileType.COMPONENT);
			}
		} else {
			JOptionPane.showMessageDialog(null, "Please, select at least one component", 
					"Unsupported opertaion", JOptionPane.WARNING_MESSAGE);
		}
	}

	/**
	 * Opens an ArchPad model.
	 */
	public void openModel() {
		openFile(EntityType.MODEL, FileType.MODEL);
	}

	/**
	 * Opens a file. The file can contain a model or component. A GUI
	 * is provided to the user from which the user selects a file to
	 * be opened. The method parameters are used to customize the 
	 * file chooser GUI.
	 *
	 * @param entityType The type of the {@link EntityType} stored in the file. 
	 * @param fileType The {@link FileType} to be opened.
	 */
	public void openFile(EntityType entityType, FileType fileType) {
		File file = chooseFile(entityType, OperationType.OPEN, fileType);
		if (file != null) {
			loadFromFile(fileType, file);
		}
	}

	/**
	 * Loads the contents of a file.
	 * 
	 * @param fileType The {@link FileType} to be loaded.
	 * @param file The {@link File} to be loaded.
	 */
	public void loadFromFile(FileType fileType, File file) {
		try {
			BufferedInputStream inputStream = new BufferedInputStream(new FileInputStream(file));
			Object[] cells = persistenceManager.loadCells(inputStream);
			if (fileType == FileType.MODEL) {
				archPad.getRegionManager().loadRegions(cells);
				archPad.getCustomizedJGraph().getGraphLayoutCache().insert(cells[0]);
				archPad.setTitle(file.getName() + " - ActPad");
			} else if (fileType == FileType.COMPONENT) {
				componentLibraryCache.put(file.getName(), cells);
			}
		} catch (FileNotFoundException fnfe) {
			String message = "The selected modelFile is either unsuported type of deas not exist";
			JOptionPane.showMessageDialog(null, message, "Invalid modelFile name", 
					JOptionPane.WARNING_MESSAGE);
		}
	}

	/**
	 * Saves an ArchPad model. If the model has no file a GUI for selecting
	 * one is provided to the user.
	 */
	public void saveModel() {
		JGraph jGraph = archPad.getCustomizedJGraph();
		Object[] cells = jGraph.getGraphLayoutCache().getCells(true, true, true, true);
		if (modelFile == null) {
			modelFile = chooseFile(EntityType.MODEL, OperationType.SAVE, FileType.MODEL);
		}
		if (modelFile != null) {
			saveCells(modelFile, cells, EntityType.MODEL, FileType.MODEL);
		}
	}

	/**
	 * Saves an ArchPad model in a new file. A GUI is shown to the user
	 * for selecting the destination file. 
	 */
	public void saveModelAs() {
		modelFile = chooseFile(EntityType.MODEL, OperationType.SAVE_AS, FileType.MODEL);
		if (modelFile != null) {
			GraphLayoutCache cache = archPad.getCustomizedJGraph().getGraphLayoutCache();
			Object[] cells = cache.getCells(true, true, true, true);
			if (cells == null) {
				saveCells(modelFile, cells, EntityType.MODEL, FileType.MODEL);
			}
		}
	}

	/**
	 * Exist the ArchPad application.
	 */
	public void exit() {
		if (modelModified) {
			int option = -1;
			option = JOptionPane.showConfirmDialog(null, "Save changes?");
			if (modelFile == null) {
				if (option == 0) {
					saveModelAs();
				}
			} else {
				if (option == 0) {
					saveModel();
				}
			}
		}
		System.exit(0);
	}

	/**
	 * Loads a component library.
	 * 
	 * @param componentList The {@link JList} that shows the 
	 *        components in the GUI.
	 */
	public void loadComponentLibary(JList componentList) {
		File[] componentFiles = componentLibraryPath.listFiles(new FileFilter() {
			public boolean accept(File fileName) {
				return fileName.getName().endsWith(".cmp");
			}
		});
		DefaultListModel model = (DefaultListModel) componentList.getModel();
		model.removeAllElements();
		if (componentFiles.length > 0) {
			for (File f : componentFiles) {
				model.addElement(f.getName());
			}
			componentList.setSelectedIndex(0);
		}
	}
	
	/*
	 * Shows a GUI for selecting a file.
	 * 
	 * @param entityType The {@link EntityType} stored in the file.
	 * @param operationType The {@link OperationType} to be performed.
	 * @param fileType The {@link FileType} to be chosen.
	 * @return The chosen file.
	 */
	private File chooseFile(EntityType entityType, OperationType operationType, 
			FileType fileType) {
		File file = null;
		JFileChooser fileChooser = new JFileChooser();
		CustomFileFilter filter = new CustomFileFilter();
		filter.addExtension(fileType.getValue());
		filter.setDescription("ArchPad " + entityType.getValue() + " files");
		fileChooser.setFileFilter(filter);
		if (fileType == FileType.FOLDER) {
			fileChooser.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY);
		} else {
			fileChooser.setFileSelectionMode(JFileChooser.FILES_ONLY);
		}
		String prefix = "untitled.";
		if (operationType == OperationType.OPEN) {
			prefix = "*.";
		}
		if (fileType == FileType.COMPONENT && componentLibraryPath != null) {
			fileChooser.setCurrentDirectory(componentLibraryPath);
		} else {
			fileChooser.setCurrentDirectory(new File(System.getProperty("user.dir")));
		}
 		fileChooser.setSelectedFile(new File(prefix + fileType.getValue()));
		fileChooser.setDialogTitle(operationType.getValue() + " " + entityType.getValue());
		fileChooser.setApproveButtonText(operationType.getValue());
		int returnVal = fileChooser.showOpenDialog(null);
		if (returnVal == JFileChooser.APPROVE_OPTION) {
			file = fileChooser.getSelectedFile();
			log.debug("Chosen file: " + fileChooser.getSelectedFile().getName());
		}
		return file;
	}

	/*
	 * Saves a an array JGraph cells to a file.
	 * 
	 * @param file The destination file.
	 * @param cells The cells to be saved.
	 * @param entityType The {@link EntityType} to be saved.
	 * @param fileType The {@link FileType} to which to save the cells.
	 */
	private void saveCells(File file, Object[] cells, EntityType entityType, 
			FileType fileType) {
		try {
			if (cells.length > 0) {
				if (fileType == FileType.COMPONENT) {
					DefaultGraphCell cell = (DefaultGraphCell)cells[0];
					ArchPadComponent archPadComponent = (ArchPadComponent)cell.getUserObject();
					archPadComponent.setTemplateFileName(file.getName());
				}
				BufferedOutputStream outputStream = new BufferedOutputStream(new FileOutputStream(file));
				persistenceManager.persisistCells(cells, outputStream);
				log.debug(entityType.getValue() + " successfully saved to modelFile " + file.getName());
			}
		} catch (FileNotFoundException fnfe) {
			log.debug("Error while saving a model", fnfe);
		}
	}

	/**
	 * Type safe enumeration for specifying a file type.
	 *
	 * @author svetoslavganov@gmail.com (Svetoslav R. Ganov)
	 *
	 */
	public enum FileType {
		MODEL("mod"), 
		COMPONENT("cmp"), 
		FOLDER(".");

		private String extension;

		private FileType(String extension) {
			this.extension = extension;
		}

		public String getValue() {
			return extension;
		}
	}

	/**
	 * Type safe enumeration for specifying an operation type.
	 *
	 * @author svetoslavganov@gmail.com (Svetoslav R. Ganov)
	 *
	 */
	public enum OperationType {
		SAVE("Save"), 
		SAVE_AS("Save As"), 
		OPEN("Open"), 
		SET_COMPONENT_LIBRARY_PATH("set component library path");

		private String name;

		private OperationType(String name) {
			this.name = name;
		}

		public String getValue() {
			return name;
		}
	}

	/**
	 * Type safe enumeration for specifying an entity type.
	 *
	 * @author svetoslavganov@gmail.com (Svetoslav R. Ganov)
	 *
	 */
	public enum EntityType {
		MODEL("model"), 
		COMPONENT("component"), 
		COMPONENT_LIBRARY_PATH("component library path");

		private String name;

		private EntityType(String name) {
			this.name = name;
		}

		public String getValue() {
			return name;
		}
	}

	public HashMap<String, Object[]> getComponentLibraryCache() {
		return componentLibraryCache;
	}
}
